npx shadcn@latest add <component> to add new componentscomponents/ui/components/features/Ikuti pola untuk optimasi navigasi dan caching:
// app/[feature]/page.tsx
import { Suspense } from 'react'
import { headers } from 'next/headers'
import { getFeatureDataAction } from '@/app/actions/feature'
import { FeatureClient } from './feature-client'
import { FeatureSkeleton } from '@/components/skeletons/feature-skeleton'
async function FeatureContent() {
// Deteksi client-side navigation untuk skip server fetch
const headersList = await headers()
const referer = headersList.get('referer') || ''
const host = headersList.get('host') || ''
const isClientNavigation = referer.includes(host) && referer.includes('/dashboard')
// Skip fetch jika client navigation - React Query akan gunakan cache
let initialData = null
if (!isClientNavigation) {
const result = await getFeatureDataAction()
initialData = result.success && result.data ? result.data : null
}
return <FeatureClient initialData={initialData} />
}
export default function FeaturePage() {
return (
<Suspense fallback={<FeatureSkeleton />}>
<FeatureContent />
</Suspense>
)
}
// app/actions/feature.ts
'use server'
import { createClient } from '@/lib/supabase/server'
export async function getFeatureDataAction() {
try {
const supabase = await createClient()
// ... fetch logic
return { success: true, data }
} catch (error) {
return { success: false, error: 'Failed to fetch' }
}
}
// lib/hooks/use-feature-data.ts
'use client'
import { useQuery, useQueryClient } from '@tanstack/react-query'
export const featureKeys = {
all: ['feature'] as const,
data: () => [...featureKeys.all, 'data'] as const,
}
export function useFeatureData<T>(initialData?: T) {
return useQuery({
queryKey: featureKeys.data(),
queryFn: async () => {
const { getFeatureDataAction } = await import('@/app/actions/feature')
const result = await getFeatureDataAction()
if (!result.success) throw new Error(result.error)
return result.data as T
},
initialData,
staleTime: 5 * 60 * 1000, // 5 menit
})
}
export function useInvalidateFeature() {
const queryClient = useQueryClient()
return () => queryClient.invalidateQueries({ queryKey: featureKeys.all })
}
// app/[feature]/feature-client.tsx
'use client'
import { useFeatureData, useInvalidateFeature } from '@/lib/hooks/use-feature-data'
interface FeatureClientProps {
initialData: FeatureData | null
}
export function FeatureClient({ initialData }: FeatureClientProps) {
const { data, isLoading } = useFeatureData(initialData || undefined)
const invalidate = useInvalidateFeature()
// Gunakan data dari React Query (dengan fallback ke initialData)
const featureData = data || initialData
// Panggil invalidate() setelah mutation
}
// Di client component setelah mutation berhasil
const invalidate = useInvalidateFeature()
const handleSave = async () => {
const result = await saveFeatureAction(data)
if (result.success) {
invalidate() // Invalidate React Query cache
}
}
headers() untuk skip server fetchuseInvalidateFeature()useAuth() hook di client, bukan server fetchapp/dashboard/page.tsx + lib/hooks/use-dashboard-data.tsapp/dashboard/settings/page.tsx + lib/hooks/use-settings-data.tsapp/admin/*/page.tsx + lib/hooks/use-admin-data.tsapp/
├── actions/ # Server actions
├── api/ # API routes
├── dashboard/ # Protected routes
│ └── [feature]/
│ ├── page.tsx # Server component
│ └── feature-client.tsx # Client component
lib/
├── db/
│ ├── data-access/ # Data fetching functions + cache tags
│ └── services/ # Database service classes
├── supabase/
│ ├── client.ts # Browser client
│ ├── server.ts # Server client
│ └── middleware.ts # Auth middleware
├── stores/ # Zustand stores
└── utils/ # Utility functions
components/
├── ui/ # shadcn/ui components
└── features/ # Feature-specific components
createClient() from @/lib/supabase/server for server-side/dashboard/*/admin/* (requires is_admin metadata)/dashboard/login, /dashboard/signup, /dashboard/forgot-password--run flag for single execution*.test.ts or *.test.tsxfast-checkserver-only package via vitest.server-only-mock.tsnpm run test'use server'
export async function myAction(data: FormData) {
const supabase = await createClient()
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) {
return { success: false, error: 'Unauthorized' }
}
// ... action logic
revalidateTag(CACHE_TAGS.feature)
revalidatePath('/dashboard/feature')
return { success: true, data: result }
}
NEXT_PUBLIC_*.env.local.env.example